require "rexml/document"
require "rexml/xpath"
require "test/unit"

class CSProj
  attr_reader :basedir, :xml
  
  @@target_ext = { "exe" => ".exe", "library" => ".dll" }
  
  def initialize(basedir, xml)
    @basedir = basedir
    @xml = xml
  end
  
  def CSProj.parse(basedir, input)
    CSProj.new basedir, REXML::Document.new(input)
  end
  
  def assembly_name
    @assembly_name ||= select_one("//PropertyGroup/AssemblyName") + @@target_ext[output_type]
  end
  
  def root_namespace
    @root_namespace ||= select_one("//PropertyGroup/RootNamespace")
  end
  
  def output_type
    @output_type ||= select_one("//PropertyGroup/OutputType").downcase
  end
  
  def signed
    if @signed.nil?
      flag = select_one("//PropertyGroup/SignAssembly")
      @signed = flag ? flag == true.to_s : false;
    end
    @signed   
  end
  
  def key_file
    @key_file ||= unixify(select_one("//PropertyGroup/AssemblyOriginatorKeyFile"))
  end
  
  def source_files
    @source_files ||= collect_files("//Compile")
  end
  
  def resource_files
    @resource_files ||= collect_files("//EmbeddedResource")
  end
  
  def if_has_resource_files?
    yield resource_files if resource_files.size != 0
  end
  
  def reference_assemblies
    @reference_assemblies ||= collect_references()
  end
  
  def refs_to_copy
    reference_assemblies.select { |ref| File.extname(ref).downcase == ".dll" }
  end
  
  def if_has_reference_assemblies?
    yield reference_assemblies if reference_assemblies.size != 0
  end
  
  def output_dir
    "bin/${configuration}"
  end
  
  def output
    File.join(output_dir, assembly_name) 
  end
  
  def csc_task(target)
    csc = target.add_element("csc", { "output" => output, "target" => output_type, "checked" => true, "debug" => "${debug}" })
    csc.add_attribute "keyfile", key_file if signed
    if_has_reference_assemblies? { |refs| populate_fileset csc.add_element("references"), refs }
    if_has_resource_files? { |resfiles| populate_fileset csc.add_element("resources", { "prefix" => root_namespace }), resfiles }
    sources = csc.add_element("sources", { "failonempty" => true })
    populate_fileset sources, source_files
    csc
  end
  
  def mkdir_task(target, dirname)
    target.add_element("mkdir", { "dir" => dirname })
  end
    
  def copy_refs_task(target)
    if refs_to_copy.size != 0
      copy = target.add_element("copy", { "todir" => output_dir, "flatten" => true })
      populate_fileset copy.add_element("fileset"), refs_to_copy
      copy
    end
  end
  
  def target(project, name)
    yield project.add_element("target", { "name" => name })
  end
  
  def project
    doc = REXML::Document.new
    p = doc.add_element("project", { "name" => assembly_name, "default" => "build", "xmlns" => "http://nant.sf.net/release/0.86-beta1/nant.xsd" })
    p.add_element("property", { "name" => "debug", "value" => false, "overwrite" => false })
    p.add_element("property", { "name" => "configuration", "value" => "Debug",  "if" => "${debug}" })
    p.add_element("property", { "name" => "configuration", "value" => "Release",  "unless" => "${debug}" })

    target(p, "build") do |t|
      mkdir_task t, output_dir 
      csc_task t
      copy_refs_task t
    end
    doc
  end
  
  private
  
  def select_one(xpath)
    node = REXML::XPath.first(@xml, xpath)
    node ? node.text : nil
  end
  
  def unixify(path)
    path.gsub "\\", "/"
  end
  
  def collect_files(xpath)
    files = []
    REXML::XPath.each(@xml, xpath) { |node| files << unixify(node.attributes["Include"]) }
    files
  end
  
  def collect_references
    refs = []
    collect_files("//ProjectReference").each do |csproj_file|
      csproj_basedir = File.dirname(csproj_file)
      csproj_path = File.join(basedir, csproj_file)
      File.open(csproj_path, "r") do |f|
        csproj = CSProj.parse(File.dirname(csproj_path), f)
        refs << File.join(csproj_basedir, csproj.output)
      end
    end
    REXML::XPath.each(@xml, "//Reference") do |node|
      hint_path = node.elements["HintPath"]
      r = hint_path ? hint_path.text : node.attributes["Include"]
      refs << unixify(r)
    end
    refs
  end
  
  def populate_fileset(fileset_element, files)
    files.each { |i| fileset_element.add_element "include", { "name" => i } }
  end
end

class CSProjTest < Test::Unit::TestCase
  def test_ctor
    CSProj.new ".", REXML::Document.new
  end
  
  def test_parse_assemblyname
    p = CSProj.parse(".", "<Project><PropertyGroup><AssemblyName>foo.bar</AssemblyName><OutputType>Library</OutputType></PropertyGroup></Project>")
    assert_equal "foo.bar.dll", p.assembly_name
  end
  
  def test_parse_rootnamespace
    p = CSProj.parse(".", "<Project><PropertyGroup><RootNamespace>somens</RootNamespace></PropertyGroup></Project>")
    assert_equal "somens", p.root_namespace
  end
  
  def test_parse_outputtype_library
    p = CSProj.parse(".", "<Project><PropertyGroup><OutputType>Library</OutputType></PropertyGroup></Project>")
    assert_equal "library", p.output_type
  end
  
  def test_parse_outputtype_exe
    p = CSProj.parse(".", "<Project><PropertyGroup><OutputType>Exe</OutputType></PropertyGroup></Project>")
    assert_equal "exe", p.output_type
  end

  def test_parse_keyfile
    p = CSProj.parse(".", "<Project><PropertyGroup><AssemblyOriginatorKeyFile>foo.snk</AssemblyOriginatorKeyFile></PropertyGroup></Project>")
    assert_equal "foo.snk", p.key_file
  end
  
  def test_parse_signed
    p = CSProj.parse(".", "<Project><PropertyGroup><SignAssembly>true</SignAssembly></PropertyGroup></Project>")
    assert p.signed
  end
  
  def test_parse_not_signed
    p = CSProj.parse(".", "<Project><PropertyGroup><SignAssembly>false</SignAssembly></PropertyGroup></Project>")
    assert !p.signed
  end
  
  def test_parse_not_signed2
    p = CSProj.parse(".", "<Project><PropertyGroup></PropertyGroup></Project>")
    assert !p.signed
  end
  
  def test_demo
    file = "C:/src/googlecode/codebackpack/trunk/plist/src/Plist.Test/Plist.Test.csproj"
    File.open(file, "r") do |f|
      p = CSProj.parse(File.dirname(file), f)
      p.project.write $stdout, 2
    end
  end
end
